Skip to content

Conversation

@mubashardev
Copy link

Resolved merge conflicts from upstream/master while preserving original branding. This PR also includes the Call Recording feature and fixes for OriginFMessageField.

Copilot AI review requested due to automatic review settings December 29, 2025 06:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a comprehensive Call Recording feature to WaEnhancer and updates repository branding from Dev4Mod to mubashardev. The changes include fixes for the OriginFMessageField lookup issue and support for newer WhatsApp versions.

Key Changes:

  • Implements call recording functionality with root and non-root modes for recording WhatsApp voice/video calls as audio
  • Adds a recordings manager UI with playback, sharing, and deletion capabilities
  • Fixes OriginFMessageField detection by searching multiple audio MIME type strings
  • Updates GitHub URLs and references throughout documentation and code

Reviewed changes

Copilot reviewed 38 out of 40 changed files in this pull request and generated 22 comments.

Show a summary per file
File Description
gradlew Standard Gradle wrapper script added
docs/README.pt-BR.md Updated GitHub repository URLs to mubashardev
docs/README.md Updated GitHub repository URLs to mubashardev
changelog.txt Documents new Call Recording feature and OriginFMessageField fix
app/src/main/res/xml/fragment_media.xml Adds Call Recording preferences UI with path selection and settings
app/src/main/res/xml/file_paths.xml FileProvider configuration for sharing recordings
app/src/main/res/values/strings_recordings.xml Comprehensive strings for recordings manager and settings
app/src/main/res/values/strings.xml Call recording related strings and controversial follower-gated feature message
app/src/main/res/values/arrays.xml Adds support for WhatsApp versions 2.25.38-40.xx
app/src/main/res/menu/bottom_nav_menu.xml Adds Recordings navigation menu item
app/src/main/res/layout/*.xml Multiple layout files for recordings UI, audio player dialog, and settings
app/src/main/res/drawable/*.xml Vector drawables for recording icons and UI elements
app/src/main/java/.../CallRecording.java Core recording logic with audio capture and WAV file generation
app/src/main/java/.../Others.java Error handling added around sendAudioType call
app/src/main/java/.../Unobfuscator.java Enhanced OriginFMessageField detection with multiple MIME type searches
app/src/main/java/.../FeatureLoader.java Registers CallRecording feature
app/src/main/java/.../RecordingsFragment.java Fragment for managing and displaying recordings
app/src/main/java/.../MediaFragment.java Adds navigation to call recording settings
app/src/main/java/.../AudioPlayerDialog.java In-app audio player for recordings
app/src/main/java/.../Recording.java Model class for recording metadata with contact resolution
app/src/main/java/.../RecordingsAdapter.java RecyclerView adapter with multi-select support
app/src/main/java/.../MainPagerAdapter.java Conditionally adds recordings tab based on preference
app/src/main/java/.../MainActivity.java Integrates recordings navigation
app/src/main/java/.../CallRecordingSettingsActivity.java Settings activity for root/non-root mode selection
app/src/main/java/.../AboutActivity.java Updates GitHub repository URL
app/src/main/AndroidManifest.xml Registers CallRecordingSettingsActivity and FileProvider
.gitignore Adds key_base64.txt to ignored files
.github/workflows/android.yml Improves secret handling and adds feature branch builds

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to +49
var prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
if (!prefs.getBoolean("call_recording_enable", false)) {
binding.navView.getMenu().findItem(R.id.navigation_recordings).setVisible(false);
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the adapter issue, the navigation menu item visibility is set once in onCreate based on the preference value. If the preference changes while the activity is running, the menu won't update. This creates an inconsistency where the navigation item visibility doesn't match the actual feature state. Consider observing preference changes and updating the menu visibility dynamically.

Copilot uses AI. Check for mistakes.
if (FMessageClass.isAssignableFrom(f.getDeclaringClass())) {
return f;
String[] commonStrings = new String[]{
"audio/ogg; codecs=opus",
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "audio/ogg; codecs=opu" but the code is searching for "audio/ogg; codecs=opus" (with an 's' at the end). While this might be intentional to fix the search, the comment is misleading and should be updated to match the actual search string or removed if it's outdated.

Copilot uses AI. Check for mistakes.
Comment on lines +269 to +285
File parentDir;
if (android.os.Environment.isExternalStorageManager()) {
parentDir = new File(android.os.Environment.getExternalStorageDirectory(), "WA Call Recordings");
} else {
String settingsPath = prefs.getString("call_recording_path", null);
if (settingsPath != null && !settingsPath.isEmpty()) {
parentDir = new File(settingsPath, "WA Call Recordings");
} else {
parentDir = new File(FeatureLoader.mApp.getExternalFilesDir(null), "Recordings");
}
}

File dir = new File(parentDir, appName + "/Voice");
if (!dir.exists() && !dir.mkdirs()) {
dir = new File(FeatureLoader.mApp.getExternalFilesDir(null), "Recordings/" + appName + "/Voice");
dir.mkdirs();
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recording path determination logic has a fallback chain, but there's no validation that any of the directories are writable or have sufficient space. This could lead to recording failures that are only discovered when writing fails. Consider checking directory permissions and available storage before starting the recording, and provide appropriate user feedback if issues are detected.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +114
new Thread(() -> {
try {
Thread.sleep(3000);
if (!isRecording.get()) {
XposedBridge.log("WaEnhancer: Starting recording after delay");
extractPhoneNumberFromCallback(callback);
isCallConnected.set(true);
startRecording();
}
} catch (Exception e) {
XposedBridge.log("WaEnhancer: Delay error: " + e.getMessage());
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling Thread.sleep(3000) directly in a new thread without handling InterruptedException properly could cause issues. The catch block catches Exception but doesn't preserve the interrupt status. If the thread is interrupted, it should either re-interrupt itself or handle the interruption appropriately. Also, there's no mechanism to cancel this delayed start if the call ends before the 3 seconds elapse.

Copilot uses AI. Check for mistakes.
echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties
echo androidStoreFile='key.jks' >> gradle.properties
echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks
echo "$KEY_STORE" | base64 --decode > key.jks
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variable KEY_STORE is now wrapped in quotes when echoed to base64 decode. However, the original code had ${{ secrets.KEY_STORE }} directly without quotes. If the KEY_STORE secret contains spaces or special characters, the quoted version is correct. But verify that this change doesn't break the base64 decoding if the secret value itself contains quotes or if the shell interprets them incorrectly.

Suggested change
echo "$KEY_STORE" | base64 --decode > key.jks
printf '%s' "$KEY_STORE" | base64 --decode > key.jks

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +115
// Hook soundPortCreated with 3 second delay to wait for call connection
XposedBridge.hookAllMethods(clsCallEventCallback, "soundPortCreated", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("WaEnhancer: soundPortCreated - will record after 3s");
extractPhoneNumberFromCallback(param.thisObject);

final Object callback = param.thisObject;
new Thread(() -> {
try {
Thread.sleep(3000);
if (!isRecording.get()) {
XposedBridge.log("WaEnhancer: Starting recording after delay");
extractPhoneNumberFromCallback(callback);
isCallConnected.set(true);
startRecording();
}
} catch (Exception e) {
XposedBridge.log("WaEnhancer: Delay error: " + e.getMessage());
}
}).start();
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardcoded 3-second delay could cause issues. If the call connects faster than 3 seconds, recording will miss the beginning. If it takes longer, recording might start before connection is established. Consider using actual call state callbacks or connection events instead of a fixed delay to ensure reliable recording start timing.

Suggested change
// Hook soundPortCreated with 3 second delay to wait for call connection
XposedBridge.hookAllMethods(clsCallEventCallback, "soundPortCreated", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("WaEnhancer: soundPortCreated - will record after 3s");
extractPhoneNumberFromCallback(param.thisObject);
final Object callback = param.thisObject;
new Thread(() -> {
try {
Thread.sleep(3000);
if (!isRecording.get()) {
XposedBridge.log("WaEnhancer: Starting recording after delay");
extractPhoneNumberFromCallback(callback);
isCallConnected.set(true);
startRecording();
}
} catch (Exception e) {
XposedBridge.log("WaEnhancer: Delay error: " + e.getMessage());
}
}).start();
// Hook soundPortCreated to start recording when audio path is ready
XposedBridge.hookAllMethods(clsCallEventCallback, "soundPortCreated", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("WaEnhancer: soundPortCreated - evaluating recording start");
extractPhoneNumberFromCallback(param.thisObject);
if (!isRecording.get()) {
XposedBridge.log("WaEnhancer: Starting recording on soundPortCreated");
extractPhoneNumberFromCallback(param.thisObject);
isCallConnected.set(true);
startRecording();
}

Copilot uses AI. Check for mistakes.
Comment on lines +253 to +257
private synchronized void startRecording() {
if (isRecording.get()) {
XposedBridge.log("WaEnhancer: Already recording");
return;
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The synchronized block protects access to randomAccessFile, but the outer check if (!isRecording.get()) is not atomic with the inner operations. This creates a race condition where two threads could both pass the outer check before either sets isRecording to true, potentially causing dual recording attempts or corrupted file writes. The entire method should be synchronized, or use a proper atomic check-and-set pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +118
boolean saved = prefs.edit().putBoolean("call_recording_use_root", true).commit();
Log.d(TAG, "Root granted, saved preference: " + saved);
Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show();
} else {
boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit();
Log.d(TAG, "Root denied, saved preference: " + saved + ", output: " + output);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using commit() instead of apply() blocks the UI thread while writing to disk. These SharedPreferences updates are happening on the UI thread (via runOnUiThread) and could cause frame drops or UI lag. Since immediate confirmation of the write isn't necessary for correctness here, use apply() for better performance.

Suggested change
boolean saved = prefs.edit().putBoolean("call_recording_use_root", true).commit();
Log.d(TAG, "Root granted, saved preference: " + saved);
Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show();
} else {
boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit();
Log.d(TAG, "Root denied, saved preference: " + saved + ", output: " + output);
prefs.edit().putBoolean("call_recording_use_root", true).apply();
Log.d(TAG, "Root granted, preference update requested.");
Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show();
} else {
prefs.edit().putBoolean("call_recording_use_root", false).apply();
Log.d(TAG, "Root denied, preference update requested, output: " + output);

Copilot uses AI. Check for mistakes.
duration = (dataSize * 1000L) / byteRate;
} else if (sampleRate > 0) {
// Assume 16-bit mono
duration = (dataSize * 1000L) / (sampleRate * 2);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential overflow in int multiplication before it is converted to long by use in a numeric context.

Suggested change
duration = (dataSize * 1000L) / (sampleRate * 2);
duration = (dataSize * 1000L) / (sampleRate * 2L);

Copilot uses AI. Check for mistakes.
return recordings.size();
}

static class ViewHolder extends RecyclerView.ViewHolder {
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ViewHolder has the same name as its supertype androidx.recyclerview.widget.RecyclerView$ViewHolder.

Copilot uses AI. Check for mistakes.
@Dev4Mod
Copy link
Owner

Dev4Mod commented Dec 29, 2025

I made some revisions to the code and there are many changes to URLs from the original project; since this is a pull request, you shouldn't make those changes.

@mubashardev
Copy link
Author

I made some revisions to the code and there are many changes to URLs from the original project; since this is a pull request, you shouldn't make those changes.

Apologies for the oversight. I've pushed a new commit that reverts the project URLs to the original ones in the documentation and About screen. I kept a small attribution only for the specific features I added. Let me know if everything looks good now!

@Bekzat158
Copy link

The call recording feature sounds great. I'm waiting for merge.

@mubashardev
Copy link
Author

mubashardev commented Dec 30, 2025

The call recording feature sounds great. I'm waiting for merge.

Appreciate it! 🙌 Hit follow button to catch future updates, and download the latest release from Releases of forked repo. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants